package jass.generators;
import jass.engine.*;
import java.net.*;

/**
   Jump through a buffer, loaded from an audio
   file or provided by caller. No speed or volume control is provided.
   Sample is divided into segments with segmentation array segs[].
   seg[k] is start of segment k, except last element of segs[], which denotes
   the end of the last segment (in order not to run into the artificial buffer end).
   Playback starts at segment i, then makes transition to setgment k, with transition
   probability tprob[i][k] (user provided matrix).
   Generally you must define first the segments, then the transition matrix.
   @author Kees van den Doel (kvdoel@cs.ubc.ca)
*/
public class GranularConstantLoopBuffer extends ConstantLoopBuffer {
    
    /** Locations of segments in samples. Last entry marks end only */
    protected int[] segs;

    /** Transition probabilities. Last segment start not transited to */
    protected double[][] tprob;

    /** Current segment index */
    protected int currentSegIndex = 0;

    /** array telling you segment index of sample k in loopBuffer[].
        This includes last unheard segment
     */
    protected int[] isSeg;

    /** Fade-in time for grains */
    protected float fadeTime = 0;

    /** If 1 will have linear wavetable playback, 0 total random */
    protected float linearBias=0;

    /** volume */
    protected float volume = 1;
    
    /** Construct from named file.
        @param srate sampling rate in Hertz.
        @param bufferSize bufferSize of this Out
        @param fn Audio file name.
    */
    public GranularConstantLoopBuffer(float srate,int bufferSize, String fn) {
        super(srate,bufferSize,fn);
    }

    /** Construct from url.
        @param srate sampling rate in Hertz.
        @param bufferSize bufferSize of this Out
        @param url Audio file url.
    */
    public GranularConstantLoopBuffer(float srate,int bufferSize, URL url) {
        super(srate,bufferSize,url);
    }

    /** Construct from provided buffer
        @param srate sampling rate in Hertz.
        @param bufferSize bufferSize of this Out.
        @param loopBuffer looping buffer.
    */
    public GranularConstantLoopBuffer(float srate,int bufferSize, float[] loopBuffer) {
        super(srate,bufferSize,loopBuffer); // this is the internal buffer size
    }

    /**
       Random initialization with n segments (not counting last unused one)
       @param n number of segments
       @param linearBias how likely a linear playback is (1 will give looping, 0 total randomness)
    */
    public void initRandom(int n,float linearBias) {
        this.linearBias = linearBias;
        setSegments(n);
        setTransitionMatrix();
    }

    /**
       Set duration of fade-in at the grain boundaries
       This method only works if the segments are ordered sequentually in time.
       @param fadeTime fade-in time in seconds
    */
    public void setFadeTime(float dur) {
        this.fadeTime = dur;
    }

    // modify loopBuffer[] to smooth out segment boundaries
    // assumes segments are ordered in time
    private void makeFadeins() {
        if(fadeTime > 0) {
            // smooth beginnings
            int ns = (int)(fadeTime*srate); // number of samples to smooth
            for(int i=0;i<segs.length-1;i++) {
                int ibegin = segs[i];
                int iend =  segs[i+1];
                for(int k=0;k<ns && ibegin+k < iend;k++) {
                    float factor = (float)(.5*(1-Math.cos(2*Math.PI*k/(srate*fadeTime))));
                    loopBuffer[ibegin+k] *= factor;
                }
            }
            // smooth endings
            for(int i=1;i<segs.length;i++) {
                int iend =  segs[i];
                int ibegin = segs[i-1];
                for(int k=0;k<ns && iend-k > ibegin;k++) {
                    float factor = (float)(.5*(1-Math.cos(2*Math.PI*k/(srate*fadeTime))));
                    loopBuffer[iend-k] *= factor;
                }
            }
        }
    }

    /** Set probability that InitRandom() generated random transition matrix
        will favour sequential playback
        @param val probability for linear order
    */
    public void setLinearBias(float val) {
        linearBias = val;
    }
    
    // cache stuff
    private void precomputeSegmentDataStructures() {
        int n = loopBuffer.length;
        isSeg = new int[n];
        for(int i=0;i<n;i++) {
            isSeg[i] = -1;
        }
        // mark beginnings
        for(int k=0;k<segs.length;k++) {
            isSeg[segs[k]] = k;
        }
        // fill rest (skipping unused begin before segment) 0
        for(int i=segs[0];i<n-1;i++) {
            if(isSeg[i+1] == -1) {
                isSeg[i+1] = isSeg[i];
            }
        }
        ix = segs[currentSegIndex]; // start here
        makeFadeins();
    }
    
    
    /**
      Define segments as float array of times.
      @param segs segments. Last segment is unused, but used to make end of prev.
    */
    public void setSegments(float[] segs) {
        this.segs = new int[segs.length];
        for(int i=0;i<segs.length;i++) {
            this.segs[i] = (int)(srate*segs[i]);
            //System.out.println("seg["+i+"]="+this.segs[i]);
        }
        precomputeSegmentDataStructures();
    }

    /**
      Define n segments randomly
      @param n  number of segments (not including last one)
    */
    public void setSegments(int n) {
        float dur = loopBuffer.length / srate;
        float s[] = new float[n+1];
        for(int i=0;i<s.length;i++) {
            s[i] = (float)(dur*(i+1)/(s.length+1));
        }
        setSegments(s);
    }

    /**
      Define transition matrix
      @param t transition matrix of size (segs.length-1)*(segs.length-1)
    */
    public void setTransitionMatrix(double[][] t) {
        this.tprob = t;
    }

    // make sure all prob. add up to one
    private void normalizeTransitionMatrix() {
        int n = segs.length - 1;
        for(int i=0;i<n;i++) {
            double iToAnywhere = 0;
            for(int k=0;k<n;k++) {
                iToAnywhere += tprob[i][k];
            }
            for(int k=0;k<n;k++) {
                tprob[i][k] /= iToAnywhere;
            }
        }
    }

    // return next segment with appropriate probability, starting from segment i
    private int getNextSegment(int sfrom) {
        int n = segs.length-1;
        double r = Math.random();
        double p = 0;
        int sto = 0;
        for(int i=0;i<n;i++) {
            p += tprob[sfrom][i];
            if(r <= p) {
                sto = i;
                break;
            }
        }
        // must have valid sto now or else probabilities were messed up
        return sto;
    }
    
    /**
      Define random transition matrix. Muse have segments defined before use.
      @param n transition matrix of size (segs.length-1)*(segs.length-1)
    */
    public void setTransitionMatrix() {
        //System.out.println("transm set with bias "+linearBias);
        int n = segs.length - 1;
        double p = 0;
        this.tprob = new double[n][n];
        // first init to linear, i.e., i->k =1 if k==i+1 (cyclically)
        for(int i=0;i<n-1;i++) {
            tprob[i][i+1] = 1;
        }
        tprob[n-1][0] = 1;
        // now add values for non-sequential transitions
        for(int i=0;i<n;i++) {
            for(int k=0;k<n;k++) {
                if(k != i+1 && !(k==0 && i==n-1)) {
                    tprob[i][k] = (1-linearBias)*Math.random();
                }
            }
        }
        normalizeTransitionMatrix();
    }

    private void incrementIx() {
        ix++;
        if(isSeg[ix] != currentSegIndex) {
            // transition
            currentSegIndex = getNextSegment(currentSegIndex);
            ix = segs[currentSegIndex];
        }
    }

    /** Set volume
        @param vol volume
    */
    public void setVolume(float vol) {
        volume = vol;
    }
    
    /** Compute the next buffer.
     */
    public void computeBuffer() {
        int bufsz = getBufferSize();
        for(int k=0;k<bufsz;k++) {
            buf[k] = volume*loopBuffer[ix];
            incrementIx();
        }
    }

}


